Warn when the capture device cannot carry continuous speech - #87
Conversation
A Bluetooth conference mic on Windows connects over the hands-free profile, which downsamples to narrowband and drops frames. faster-whisper then returns garbled text. The same mic over USB Audio Class transcribes cleanly, so this is the transport, not the model. What made it cost an afternoon is that wake detection does not degrade with it. A live "computah ..." still scores 0.998, because the wake model matches a short fixed pattern that survives the damage while a full sentence does not. The loop looks healthy right up to the transcript, so the mic is the last thing suspected. Hence a warning at startup rather than only a README note. Two signals: a hands-free marker in the device name, and a native rate below the pipeline's 16 kHz. The rate read is the device's advertised rate, not the one the stream opened at -- _open() asks for 16 kHz first and WASAPI auto_convert grants it even on a narrowband device, so the opened rate reports 16000 while the audio is already ruined. Checking that field would have detected nothing. The classifier is its own module rather than part of audio.py because audio.py imports sounddevice, which loads native PortAudio and is absent on plenty of boxes. Judging a device's advertised properties needs no hardware, so keeping it separate makes it testable everywhere, matching the hardware-free-core split the project already draws. Also surfaced where someone choosing a mic will look: --list tags unsuitable inputs, and --test-mic now exits 3 instead of reporting a clean pass on a device it just warned about. A diagnostic that returns success on hardware it told you not to use is how this stayed confusing. Known blind spot, documented rather than papered over: wideband HFP negotiates 16 kHz and clears the rate check, and on Linux the name marker is usually absent, so a clean result means "nothing detected", not "device is fine". wake-20260727T1000-0b9a6b
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 394a805245
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The rate check read default_samplerate and then asserted the device 'is narrowband' and told the operator to use a different mic. That field is what the device advertises, not a measured capability: under WASAPI it is the shared-mode capture format and the limit is real, but under ALSA or CoreAudio it can be a default the device exceeds, so the flat claim could send someone to replace a working mic. Reports the advertised figure and what it does and does not establish per host, and softens --test-mic's RESULT line and exit-3 help to read as the classifier's verdict rather than as ground truth. The hands-free branch keeps its certainty, since a name match identifies the device outright. Kept consulting the advertised rate rather than the opened one: a successful open at 16 kHz is what auto_convert fakes, which is the bug this feature exists to catch. Separating an advertised default from a hard limit needs a spectral check on real audio, and that costs a capture at startup. Detection unchanged, so the 23 checks still pass.
|
@codex review |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
The name and rate checks only see what a device chooses to report, which is enough on Windows and not enough anywhere else. A bluez/PipeWire HFP source on Linux exposes no hands-free marker and PipeWire resamples to 16 kHz and reports 16 kHz, so both existing signals read clean while the audio underneath came off an 8 kHz transport. That hole was documented in the module and left open. --test-mic already captures a few seconds, so judging that audio costs nothing extra. Upsampling cannot invent content it never received, so an 8 kHz source leaves the band above 4 kHz empty and the cliff at that edge is the evidence. Measuring the cliff rather than total high-band energy is the part that took work. Speech falls off steeply on its own, so a raw high-band fraction mostly measures the tilt: on these signals a dull voice reads 0.00007% and a sloppily resampled 8 kHz signal reads 0.005%, putting the genuine case on the wrong side of the narrowband one. Comparing the 1 kHz band above the edge against the 1 kHz below cancels the tilt and separates them by an order of magnitude in the right order. The threshold is measured rather than guessed, and _cliff_ratio is split out from the verdict so a test can assert on the margin instead of only the verdict. That test earned its place immediately: the first threshold sat under 2x from the narrowband ceiling and had to move. Refs #34. wake-20260727T1200-f460a6
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 01f169cbc7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7b5c3f8cde
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@codex review |
|
Codex Review: Didn't find any major issues. Nice work! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback". |
Closes #34.
What was wrong
A Bluetooth conference mic on Windows connects over the hands-free profile (HFP), which downsamples to narrowband and drops frames. faster-whisper receives that and returns garbled transcripts. The same mic over USB Audio Class transcribes cleanly, so the fault is the transport.
The part that cost real time: wake detection does not degrade with it. A live "computah ..." still scores 0.998, because the wake model matches a short fixed pattern that survives the damage while a full sentence does not. The loop looks healthy right up to the transcript, so the mic is the last thing anyone suspects. That is why this warns at startup rather than only in the README.
What changed
capture_quality.py(new):assess_input_device(name, native_sr, target_sr)returns aCaptureRiskorNone. Two signals, name first because the profile is the thing to act on and the rate is its symptom:hands-free,handsfree,hfp)audio.py:Microphonenow recordsnative_srandcapture_risk.--listtags unsuitable inputs and prints why;--test-micreportsnative_srnext toopen_srand exits 3.pipeline.py: the live loop prints the warning once at startup, then keeps running (the device still serves wake detection fine).--test-micexit codes.The detail that makes this work
It reads the device's advertised rate, not the rate the stream opened at.
Microphone._open()asks for 16 kHz first, and WASAPIauto_convertgrants it even on a narrowband device — so_open_srreports a healthy16000while the audio is already ruined. A check against the opened rate would have detected nothing. That trap is now named in the field's own comment so it does not get "simplified" later.Why the classifier is its own module
audio.pyimportssounddevice, which loads native PortAudio and is absent on plenty of boxes (including the one this was written on). Judging a device's advertised properties needs no hardware, so keeping it separate makes it testable everywhere — which matches the split the project already draws, where the pipeline core is hardware-free andaudio.pyis the one OS-specific seam.Deliberate non-decisions
<=instead of<would flag the one device known to work, so a test pins it.Known blind spot, documented not papered over
Wideband HFP negotiates 16 kHz, so it clears the rate check; and on Linux a bluez/PipeWire HFP source usually carries no hands-free marker, so the name check never fires there. A clean result means "nothing detected", not "device is fine" — stated in the module docstring and the README. Catching that case needs a spectral check on real audio, which costs a capture at startup.
Verification
test_capture_quality.py(new, added to the CI fast-test list): 23 checks. Covers the classifier's verdicts, the good devices staying silent, unparseable rate input, and — the half that makes the feature real — that the warning actually reaches stdout at--listenstartup, once, without stopping the loop.ruff checkandruff format --checkclean; all 17 CI fast tests pass locally.<to<=fails exactly the Pi-PowerConf case, and suppressing the startup print fails exactly the three surfacing checks. Neither test passes vacuously.wake-20260727T1000-0b9a6b